package org.easypeas.http;

import java.io.IOException;
import java.net.MalformedURLException;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.easypeas.EasyPeasConstants;
import org.easypeas.exceptions.InvokerNotFoundException;
import org.easypeas.exceptions.ResourceInvocationFailure;
import org.easypeas.exceptions.UnableToDetermineTemplateException;
import org.easypeas.exceptions.UnknownResourceException;
import org.easypeas.exceptions.UnresolvedMethodException;
import org.easypeas.invocation.InvocationFactory;
import org.easypeas.invocation.Invoker;
import org.easypeas.mappings.Mappings;
import org.easypeas.mappings.MappingsFactory;
import org.easypeas.templates.TemplateFinder;
import org.easypeas.templates.TemplateFinderFactory;

/**
 * <p>
 * Main handler for handling EasyPea paths. Behaves as a singleton and should be
 * accessed via getInstance()
 * </p>
 *
 * @author S Owen
 *
 */
public class EasyPeasHandler {
    
    private static VelocityEngine engine = new VelocityEngine();
    
    // FIXME: Are these shared resources thread safe??
    private static Mappings mappings;
    
    private static Invoker invoker;
    
    private static TemplateFinder templateFinder;
    
    private static Logger logger = Logger.getLogger("org.easypeas.http.EasyPeasHandler");
    
    private static EasyPeasHandler instance = new EasyPeasHandler();
    
    /**
     * Singleton getInstance pattern
     * @return the EasyPeas singleton instance.
     */
    public static EasyPeasHandler getInstance() {
        if (mappings == null) {
            logger
                    .severe("Mappings are not yet initailsed, you should call init(...) first");
        }
        if (invoker == null) {
            logger
                    .severe("Invoker is not yet initailsed, you should call init(...) first");
        }
        if (templateFinder == null) {
            logger
                    .severe("Template Finder is not yet initailsed, you should call init(...) first");
        }
        return instance;
    }
    
    /**
     * Constructor, private to enforce singleton pattern
     */
    private EasyPeasHandler() {
        
    }
    
    /**
     * Initialises the handler with details from context.<br>
     * This should be called before the first call to getInstance() and using an
     * instance of the handler.
     *
     * @param context the ServletContext
     * @param templatePath the template path
     *            the path to the templates directory, relative to the webapp
     *            root.
     * @throws ServletException if something goes wrong
     */
    public static void init(ServletContext context, String templatePath)
    throws ServletException {
        
        templateFinder = TemplateFinderFactory.getInstance().finder();
        try {
            invoker = InvocationFactory.getInstance().invoker();
        } catch (InvokerNotFoundException e) {
            logger.log(Level.SEVERE,"Unable to find invoker",e);
            invoker = e.getDefaultInvoker();
        }
        
        try {
            mappings = MappingsFactory.getInstance().mappings(
                    context.getResource("/WEB-INF/easypeas.classes"));
        } catch (IOException e) {
            logger.log(Level.SEVERE,"Error creating mappings", e);
            throw new ServletException(e);
        }
        
        String templateResourcePath = context.getRealPath(templatePath);
        engine.addProperty("file.resource.loader.path", templateResourcePath);
    }
    
    /**
     * Tests whether the path is one that should be handled by this handler.
     *
     * @param path the path
     * @param context the ServletContext
     * @return boolean indicating if the path should be handled
     * @throws MalformedURLException if the path cannot be used as a URI to open a resource stream
     */
    public static boolean canHandle(String path, ServletContext context)
    throws MalformedURLException {
        
        boolean result = true;
        if (path.length() == 0 || "/".equals(path)) {
            logger.finer("Empty path detected, this will be passed to default dispatcher");
            result = false;
        } else if (path.contains(".")) {
            logger.finer("Found path containing a dot, checking for physical file, path="
                    + path);
            if (context.getResource(path) != null) {
                logger.finer("physical file found for path=" + path);
                result = false;
            }
        }
        return result;
    }
    
    /**
     * Handles an Action.
     *
     * @param request the HttpServletRequest
     * @param response the HttpServletResponse
     * @param path the path
     * @throws ServletException if anything goes wrong
     */
    public void handleAction(HttpServletRequest request,
            HttpServletResponse response, String path) throws ServletException {
        try {
            Map<String, Object> map = invoker.invokeActionPath(request,
                    response, mappings, path);
            String forwardPath = map.get(EasyPeasConstants.EASYPEA_FORWARD)
            .toString();
            if (forwardPath.endsWith(".vm")) {
                logger.finer("Processing forward as template:"
                            + forwardPath);
                processTemplate(response, map, forwardPath);
            } else {
                logger.finer("Processing forward as path:" + forwardPath);
                map.remove(EasyPeasConstants.EASYPEA_FORWARD);
                map.putAll(invoker.invokePath(request, response, mappings,
                        forwardPath));
                selectTemplateAndDisplay(response, map);
            }
        } catch (Exception e) {
            forwardToError(response, e);
        }
    }
    
    /**
     * Handles a View.
     *
     * @param request the HttpServletRequest
     * @param response the HttpServletResponse
     * @param path the path
     * @throws ServletException if anything goes wrong
     */
    public void handleView(HttpServletRequest request,
            HttpServletResponse response, String path) throws ServletException {
        Map<String, Object> resultMap = null;
        try {
            resultMap = invoke(request, response, path);
            
            // add the handler for importing other EasyPea paths
            resultMap.put(EasyPeasConstants.VIEWHANDER_KEY,
                    new ViewHandlerBean(request, response));
            selectTemplateAndDisplay(response, resultMap);
        } catch (Exception e) {
            forwardToError(response, e);
        }
    }
    
    private void forwardToError(HttpServletResponse response, Throwable e)
    throws ServletException {
        logger.log(Level.FINER,"Forwarding error",e);
        Map<String, Object> errors = new HashMap<String, Object>();
        errors.put("exception", e);
        VelocityContext errorContext = new VelocityContext(errors);
        
        try {
            Template errorTemplate = engine.getTemplate(errorTemplateName());
            errorTemplate.merge(errorContext, response.getWriter());
        } catch (Exception e1) {
            logger.log(Level.SEVERE,"Unable to find error template", e1);
            throw new ServletException(e);
        }
    }
    
    private String errorTemplateName() {
        return "error.vm";
    }
    
    private void selectTemplateAndDisplay(HttpServletResponse response,
            Map<String, Object> resultMap) throws ServletException {
        
        try {
            String templateName = determineTemplateName(resultMap);
            logger.finer("Template name determined as " + templateName);
            processTemplate(response, resultMap, templateName);
        } catch (Exception e1) {
            forwardToError(response, e1);
        }
    }
    
    private void processTemplate(HttpServletResponse response,
            Map<String, Object> resultMap, String templateName)
            throws Exception, IOException {
        VelocityContext context = new VelocityContext(resultMap);
        Template template = engine.getTemplate(templateName);
        template.merge(context, response.getWriter());
    }
    
    private Map<String, Object> invoke(HttpServletRequest request,
            HttpServletResponse response, String path)
            throws UnresolvedMethodException, UnknownResourceException,
            ResourceInvocationFailure {
        Map<String, Object> result = null;
        
        logger.finer("Path=" + path);
        result = invoker.invokePath(request, response, mappings, path);
        
        return result;
    }
    
    private String determineTemplateName(Map<String, Object> peaMap)
    throws UnableToDetermineTemplateException {
        return templateFinder.find(peaMap, mappings);
    }
    
}
